Move text history tests
authorMatthias Clasen <mclasen@redhat.com>
Wed, 26 May 2021 11:11:47 +0000 (07:11 -0400)
committerMatthias Clasen <mclasen@redhat.com>
Wed, 26 May 2021 11:34:28 +0000 (07:34 -0400)
We can test internal apis in our testsuite now,
so move the text history tests there, to have them
run in ci.

tests/meson.build
tests/testtexthistory.c [deleted file]
testsuite/gtk/meson.build
testsuite/gtk/texthistory.c [new file with mode: 0644]

index 8ea23bf3fd3b86fc0573f970898cc62da514c2bf..b548db4e7a465be5109549e3a2d4e1b9c644170d 100644 (file)
@@ -114,7 +114,6 @@ gtk_tests = [
   ['testtexture'],
   ['testwindowdrag'],
   ['testinhibitshortcuts'],
-  ['testtexthistory', ['../gtk/gtktexthistory.c']],
   ['testzoom']
 ]
 
diff --git a/tests/testtexthistory.c b/tests/testtexthistory.c
deleted file mode 100644 (file)
index 556b2ac..0000000
+++ /dev/null
@@ -1,600 +0,0 @@
-#include "gtktexthistoryprivate.h"
-
-#if 0
-# define DEBUG_COMMANDS
-#endif
-
-typedef struct
-{
-  GtkTextHistory *history;
-  GString *buf;
-  struct {
-    int insert;
-    int bound;
-  } selection;
-  guint can_redo : 1;
-  guint can_undo : 1;
-  guint is_modified : 1;
-} Text;
-
-enum {
-  IGNORE = 0,
-  SET = 1,
-  UNSET = 2,
-};
-
-enum {
-  IGNORE_SELECT = 0,
-  DO_SELECT = 1,
-};
-
-enum {
-  INSERT = 1,
-  INSERT_SEQ,
-  BACKSPACE,
-  DELETE_KEY,
-  UNDO,
-  REDO,
-  BEGIN_IRREVERSIBLE,
-  END_IRREVERSIBLE,
-  BEGIN_USER,
-  END_USER,
-  MODIFIED,
-  UNMODIFIED,
-  SELECT,
-  CHECK_SELECT,
-  SET_MAX_UNDO,
-};
-
-typedef struct
-{
-  int kind;
-  int location;
-  int end_location;
-  const char *text;
-  const char *expected;
-  int can_undo;
-  int can_redo;
-  int is_modified;
-  int select;
-} Command;
-
-static void
-do_change_state (gpointer funcs_data,
-                 gboolean is_modified,
-                 gboolean can_undo,
-                 gboolean can_redo)
-{
-  Text *text = funcs_data;
-
-  text->is_modified = is_modified;
-  text->can_undo = can_undo;
-  text->can_redo = can_redo;
-}
-
-static void
-do_insert (gpointer    funcs_data,
-           guint       begin,
-           guint       end,
-           const char *text,
-           guint       len)
-{
-  Text *t = funcs_data;
-
-#ifdef DEBUG_COMMANDS
-  g_printerr ("Insert Into '%s' (begin=%u end=%u text=%s)\n",
-              t->buf->str, begin, end, text);
-#endif
-
-  g_string_insert_len (t->buf, begin, text, len);
-}
-
-static void
-do_delete (gpointer     funcs_data,
-           guint        begin,
-           guint        end,
-           const char *expected_text,
-           guint        len)
-{
-  Text *t = funcs_data;
-
-#ifdef DEBUG_COMMANDS
-  g_printerr ("Delete(begin=%u end=%u expected_text=%s)\n", begin, end, expected_text);
-#endif
-
-  if (end < begin)
-    {
-      guint tmp = end;
-      end = begin;
-      begin = tmp;
-    }
-
-  g_assert_cmpint (memcmp (t->buf->str + begin, expected_text, len), ==, 0);
-
-  if (end >= t->buf->len)
-    {
-      t->buf->len = begin;
-      t->buf->str[begin] = 0;
-      return;
-    }
-
-  memmove (t->buf->str + begin,
-           t->buf->str + end,
-           t->buf->len - end);
-  g_string_truncate (t->buf, t->buf->len - (end - begin));
-}
-
-static void
-do_select (gpointer funcs_data,
-           int      selection_insert,
-           int      selection_bound)
-{
-  Text *text = funcs_data;
-
-  text->selection.insert = selection_insert;
-  text->selection.bound = selection_bound;
-}
-
-static GtkTextHistoryFuncs funcs = {
-  do_change_state,
-  do_insert,
-  do_delete,
-  do_select,
-};
-
-static Text *
-text_new (void)
-{
-  Text *text = g_slice_new0 (Text);
-
-  text->history = gtk_text_history_new (&funcs, text);
-  text->buf = g_string_new (NULL);
-  text->selection.insert = -1;
-  text->selection.bound = -1;
-
-  return text;
-}
-
-static void
-text_free (Text *text)
-{
-  g_object_unref (text->history);
-  g_string_free (text->buf, TRUE);
-  g_slice_free (Text, text);
-}
-
-static void
-command_insert (const Command *cmd,
-                Text          *text)
-{
-  do_insert (text,
-             cmd->location,
-             cmd->location + g_utf8_strlen (cmd->text, -1),
-             cmd->text,
-             strlen (cmd->text));
-  gtk_text_history_text_inserted (text->history, cmd->location, cmd->text, -1);
-}
-
-static void
-command_delete_key (const Command *cmd,
-                    Text          *text)
-{
-  do_delete (text,
-             cmd->location,
-             cmd->end_location,
-             cmd->text,
-             strlen (cmd->text));
-  gtk_text_history_text_deleted (text->history,
-                                 cmd->location,
-                                 cmd->end_location,
-                                 cmd->text,
-                                 ABS (cmd->end_location - cmd->location));
-}
-
-static void
-command_undo (const Command *cmd,
-              Text          *text)
-{
-  gtk_text_history_undo (text->history);
-}
-
-static void
-command_redo (const Command *cmd,
-              Text          *text)
-{
-  gtk_text_history_redo (text->history);
-}
-
-static void
-set_selection (Text *text,
-               int   begin,
-               int   end)
-{
-  gtk_text_history_selection_changed (text->history, begin, end);
-}
-
-static void
-run_test (const Command *commands,
-          guint          n_commands,
-          guint          max_undo)
-{
-  Text *text = text_new ();
-
-  if (max_undo)
-    gtk_text_history_set_max_undo_levels (text->history, max_undo);
-
-  for (guint i = 0; i < n_commands; i++)
-    {
-      const Command *cmd = &commands[i];
-
-#ifdef DEBUG_COMMANDS
-      g_printerr ("%d: %d\n", i, cmd->kind);
-#endif
-
-      switch (cmd->kind)
-        {
-        case INSERT:
-          command_insert (cmd, text);
-          break;
-
-        case INSERT_SEQ:
-          for (guint j = 0; cmd->text[j]; j++)
-            {
-              const char seqtext[2] = { cmd->text[j], 0 };
-              Command seq = { INSERT, cmd->location + j, 1, seqtext, NULL };
-              command_insert (&seq, text);
-            }
-          break;
-
-        case DELETE_KEY:
-          if (cmd->select == DO_SELECT)
-            set_selection (text, cmd->location, cmd->end_location);
-          else if (strlen (cmd->text) == 1)
-            set_selection (text, cmd->location, -1);
-          else
-            set_selection (text, -1, -1);
-          command_delete_key (cmd, text);
-          break;
-
-        case BACKSPACE:
-          if (cmd->select == DO_SELECT)
-            set_selection (text, cmd->location, cmd->end_location);
-          else if (strlen (cmd->text) == 1)
-            set_selection (text, cmd->end_location, -1);
-          else
-            set_selection (text, -1, -1);
-          command_delete_key (cmd, text);
-          break;
-
-        case UNDO:
-          command_undo (cmd, text);
-          break;
-
-        case REDO:
-          command_redo (cmd, text);
-          break;
-
-        case BEGIN_USER:
-          gtk_text_history_begin_user_action (text->history);
-          break;
-
-        case END_USER:
-          gtk_text_history_end_user_action (text->history);
-          break;
-
-        case BEGIN_IRREVERSIBLE:
-          gtk_text_history_begin_irreversible_action (text->history);
-          break;
-
-        case END_IRREVERSIBLE:
-          gtk_text_history_end_irreversible_action (text->history);
-          break;
-
-        case MODIFIED:
-          gtk_text_history_modified_changed (text->history, TRUE);
-          break;
-
-        case UNMODIFIED:
-          gtk_text_history_modified_changed (text->history, FALSE);
-          break;
-
-        case SELECT:
-          gtk_text_history_selection_changed (text->history,
-                                              cmd->location,
-                                              cmd->end_location);
-          break;
-
-        case CHECK_SELECT:
-          g_assert_cmpint (text->selection.insert, ==, cmd->location);
-          g_assert_cmpint (text->selection.bound, ==, cmd->end_location);
-          break;
-
-        case SET_MAX_UNDO:
-          /* Not ideal use of location, but fine */
-          gtk_text_history_set_max_undo_levels (text->history, cmd->location);
-          break;
-
-        default:
-          break;
-        }
-
-      if (cmd->expected)
-        g_assert_cmpstr (text->buf->str, ==, cmd->expected);
-
-      if (cmd->can_redo == SET)
-        g_assert_cmpint (text->can_redo, ==, TRUE);
-      else if (cmd->can_redo == UNSET)
-        g_assert_cmpint (text->can_redo, ==, FALSE);
-
-      if (cmd->can_undo == SET)
-        g_assert_cmpint (text->can_undo, ==, TRUE);
-      else if (cmd->can_undo == UNSET)
-        g_assert_cmpint (text->can_undo, ==, FALSE);
-
-      if (cmd->is_modified == SET)
-        g_assert_cmpint (text->is_modified, ==, TRUE);
-      else if (cmd->is_modified == UNSET)
-        g_assert_cmpint (text->is_modified, ==, FALSE);
-    }
-
-  text_free (text);
-}
-
-static void
-test1 (void)
-{
-  static const Command commands[] = {
-    { INSERT, 0, -1, "test", "test", SET, UNSET },
-    { INSERT, 2, -1, "s", "tesst", SET, UNSET },
-    { INSERT, 3, -1, "ss", "tesssst", SET, UNSET },
-    { DELETE_KEY, 2, 5, "sss", "test", SET, UNSET },
-    { UNDO, -1, -1, NULL, "tesssst", SET, SET },
-    { REDO, -1, -1, NULL, "test", SET, UNSET },
-    { UNDO, -1, -1, NULL, "tesssst", SET, SET },
-    { DELETE_KEY, 0, 7, "tesssst", "", SET, UNSET },
-    { INSERT, 0, -1, "z", "z", SET, UNSET },
-    { UNDO, -1, -1, NULL, "", SET, SET },
-    { UNDO, -1, -1, NULL, "tesssst", SET, SET },
-    { UNDO, -1, -1, NULL, "test", SET, SET },
-  };
-
-  run_test (commands, G_N_ELEMENTS (commands), 0);
-}
-
-static void
-test2 (void)
-{
-  static const Command commands[] = {
-    { BEGIN_IRREVERSIBLE, -1, -1, NULL, "", UNSET, UNSET },
-    { INSERT, 0, -1, "this is a test", "this is a test", UNSET, UNSET },
-    { END_IRREVERSIBLE, -1, -1, NULL, "this is a test", UNSET, UNSET },
-    { UNDO, -1, -1, NULL, "this is a test", UNSET, UNSET },
-    { REDO, -1, -1, NULL, "this is a test", UNSET, UNSET },
-    { BEGIN_USER, -1, -1, NULL, NULL, UNSET, UNSET },
-    { INSERT, 0, -1, "first", "firstthis is a test", UNSET, UNSET },
-    { INSERT, 5, -1, " ", "first this is a test", UNSET, UNSET },
-    { END_USER, -1, -1, NULL, "first this is a test", SET, UNSET },
-    { UNDO, -1, -1, NULL, "this is a test", UNSET, SET },
-    { UNDO, -1, -1, NULL, "this is a test", UNSET, SET },
-    { REDO, -1, -1, NULL, "first this is a test", SET, UNSET },
-    { UNDO, -1, -1, NULL, "this is a test", UNSET, SET },
-  };
-
-  run_test (commands, G_N_ELEMENTS (commands), 0);
-}
-
-static void
-test3 (void)
-{
-  static const Command commands[] = {
-    { INSERT_SEQ, 0, -1, "this is a test of insertions.", "this is a test of insertions.", SET, UNSET },
-    { UNDO, -1, -1, NULL, "this is a test of", SET, SET },
-    { UNDO, -1, -1, NULL, "this is a test", SET, SET },
-    { UNDO, -1, -1, NULL, "this is a", SET, SET },
-    { UNDO, -1, -1, NULL, "this is", SET, SET },
-    { UNDO, -1, -1, NULL, "this", SET, SET },
-    { UNDO, -1, -1, NULL, "", UNSET, SET },
-    { UNDO, -1, -1, NULL, "" , UNSET, SET },
-    { REDO, -1, -1, NULL, "this", SET, SET },
-    { REDO, -1, -1, NULL, "this is", SET, SET },
-    { REDO, -1, -1, NULL, "this is a", SET, SET },
-    { REDO, -1, -1, NULL, "this is a test", SET, SET },
-    { REDO, -1, -1, NULL, "this is a test of", SET, SET },
-    { REDO, -1, -1, NULL, "this is a test of insertions.", SET, UNSET },
-  };
-
-  run_test (commands, G_N_ELEMENTS (commands), 0);
-}
-
-static void
-test4 (void)
-{
-  static const Command commands[] = {
-    { INSERT, 0, -1, "initial text", "initial text", SET, UNSET },
-    /* Barrier */
-    { BEGIN_IRREVERSIBLE, -1, -1, NULL, NULL, UNSET, UNSET },
-    { END_IRREVERSIBLE, -1, -1, NULL, NULL, UNSET, UNSET },
-    { INSERT, 0, -1, "more text ", "more text initial text", SET, UNSET },
-    { UNDO, -1, -1, NULL, "initial text", UNSET, SET },
-    { UNDO, -1, -1, NULL, "initial text", UNSET, SET },
-    { REDO, -1, -1, NULL, "more text initial text", SET, UNSET },
-    /* Barrier */
-    { BEGIN_IRREVERSIBLE, UNSET, UNSET },
-    { END_IRREVERSIBLE, UNSET, UNSET },
-    { UNDO, -1, -1, NULL, "more text initial text", UNSET, UNSET },
-  };
-
-  run_test (commands, G_N_ELEMENTS (commands), 0);
-}
-
-static void
-test5 (void)
-{
-  static const Command commands[] = {
-    { INSERT, 0, -1, "initial text", "initial text", SET, UNSET },
-    { DELETE_KEY, 0, 12, "initial text", "", SET, UNSET },
-    /* Add empty nested user action (should get ignored) */
-    { BEGIN_USER, -1, -1, NULL, NULL, UNSET, UNSET },
-      { BEGIN_USER, -1, -1, NULL, NULL, UNSET, UNSET },
-        { BEGIN_USER, -1, -1, NULL, NULL, UNSET, UNSET },
-        { END_USER, -1, -1, NULL, NULL, UNSET, UNSET },
-      { END_USER, -1, -1, NULL, NULL, UNSET, UNSET },
-    { END_USER, -1, -1, NULL, NULL, SET, UNSET },
-    { UNDO, -1, -1, NULL, "initial text" },
-  };
-
-  run_test (commands, G_N_ELEMENTS (commands), 0);
-}
-
-static void
-test6 (void)
-{
-  static const Command commands[] = {
-    { INSERT_SEQ, 0, -1, " \t\t    this is some text", " \t\t    this is some text", SET, UNSET },
-    { UNDO, -1, -1, NULL, " \t\t    this is some", SET, SET },
-    { UNDO, -1, -1, NULL, " \t\t    this is", SET, SET },
-    { UNDO, -1, -1, NULL, " \t\t    this", SET, SET },
-    { UNDO, -1, -1, NULL, "", UNSET, SET },
-    { UNDO, -1, -1, NULL, "", UNSET, SET },
-  };
-
-  run_test (commands, G_N_ELEMENTS (commands), 0);
-}
-
-static void
-test7 (void)
-{
-  static const Command commands[] = {
-    { MODIFIED, -1, -1, NULL, NULL, UNSET, UNSET, SET },
-    { UNMODIFIED, -1, -1, NULL, NULL, UNSET, UNSET, UNSET },
-    { INSERT, 0, -1, "foo bar", "foo bar", SET, UNSET, UNSET },
-    { MODIFIED, -1, -1, NULL, NULL, SET, UNSET, SET },
-    { UNDO, -1, -1, NULL, "", UNSET, SET, UNSET },
-    { REDO, -1, -1, NULL, "foo bar", SET, UNSET, SET },
-    { UNDO, -1, -1, NULL, "", UNSET, SET, UNSET },
-    { REDO, -1, -1, NULL, "foo bar", SET, UNSET, SET },
-  };
-
-  run_test (commands, G_N_ELEMENTS (commands), 0);
-}
-
-static void
-test8 (void)
-{
-  static const Command commands[] = {
-    { INSERT, 0, -1, "foo bar", "foo bar", SET, UNSET, UNSET },
-    { MODIFIED, -1, -1, NULL, NULL, SET, UNSET, SET },
-    { INSERT, 0, -1, "f", "ffoo bar", SET, UNSET, SET },
-    { UNMODIFIED, -1, -1, NULL, NULL, SET, UNSET, UNSET },
-    { UNDO, -1, -1, NULL, "foo bar", SET, SET, SET },
-    { UNDO, -1, -1, NULL, "", UNSET, SET, SET },
-    { REDO, -1, -1, NULL, "foo bar", SET, SET, SET },
-    { REDO, -1, -1, NULL, "ffoo bar", SET, UNSET, UNSET },
-  };
-
-  run_test (commands, G_N_ELEMENTS (commands), 0);
-}
-
-static void
-test9 (void)
-{
-  static const Command commands[] = {
-    { INSERT, 0, -1, "foo bar", "foo bar", SET, UNSET, UNSET },
-    { DELETE_KEY, 0, 3, "foo", " bar", SET, UNSET, UNSET, DO_SELECT },
-    { DELETE_KEY, 0, 4, " bar", "", SET, UNSET, UNSET, DO_SELECT },
-    { UNDO, -1, -1, NULL, " bar", SET, SET, UNSET },
-    { CHECK_SELECT, 0, 4, NULL, " bar", SET, SET, UNSET },
-    { UNDO, -1, -1, NULL, "foo bar", SET, SET, UNSET },
-    { CHECK_SELECT, 0, 3, NULL, "foo bar", SET, SET, UNSET },
-    { BEGIN_IRREVERSIBLE, -1, -1, NULL, "foo bar", UNSET, UNSET, UNSET },
-    { END_IRREVERSIBLE, -1, -1, NULL, "foo bar", UNSET, UNSET, UNSET },
-  };
-
-  run_test (commands, G_N_ELEMENTS (commands), 0);
-}
-
-static void
-test10 (void)
-{
-  static const Command commands[] = {
-    { BEGIN_USER }, { INSERT, 0, -1, "t", "t", UNSET, UNSET, UNSET }, { END_USER },
-    { BEGIN_USER }, { INSERT, 1, -1, " ", "t ", UNSET, UNSET, UNSET }, { END_USER },
-    { BEGIN_USER }, { INSERT, 2, -1, "t", "t t", UNSET, UNSET, UNSET }, { END_USER },
-    { BEGIN_USER }, { INSERT, 3, -1, "h", "t th", UNSET, UNSET, UNSET }, { END_USER },
-    { BEGIN_USER }, { INSERT, 4, -1, "i", "t thi", UNSET, UNSET, UNSET }, { END_USER },
-    { BEGIN_USER }, { INSERT, 5, -1, "s", "t this", UNSET, UNSET, UNSET }, { END_USER },
-  };
-
-  run_test (commands, G_N_ELEMENTS (commands), 0);
-}
-
-static void
-test11 (void)
-{
-  /* Test backspace */
-  static const Command commands[] = {
-    { INSERT_SEQ, 0, -1, "insert some text", "insert some text", SET, UNSET, UNSET },
-    { BACKSPACE, 15, 16, "t", "insert some tex", SET, UNSET, UNSET },
-    { BACKSPACE, 14, 15, "x", "insert some te", SET, UNSET, UNSET },
-    { BACKSPACE, 13, 14, "e", "insert some t", SET, UNSET, UNSET },
-    { BACKSPACE, 12, 13, "t", "insert some ", SET, UNSET, UNSET },
-    { UNDO, -1, -1, NULL, "insert some text", SET, SET, UNSET },
-  };
-
-  run_test (commands, G_N_ELEMENTS (commands), 0);
-}
-
-static void
-test12 (void)
-{
-  static const Command commands[] = {
-    { INSERT_SEQ, 0, -1, "this is a test\nmore", "this is a test\nmore", SET, UNSET, UNSET },
-    { UNDO, -1, -1, NULL, "this is a test\n", SET, SET, UNSET },
-    { UNDO, -1, -1, NULL, "this is a test", SET, SET, UNSET },
-    { UNDO, -1, -1, NULL, "this is a", SET, SET, UNSET },
-    { UNDO, -1, -1, NULL, "this is", SET, SET, UNSET },
-    { UNDO, -1, -1, NULL, "this", SET, SET, UNSET },
-    { UNDO, -1, -1, NULL, "", UNSET, SET, UNSET },
-  };
-
-  run_test (commands, G_N_ELEMENTS (commands), 0);
-}
-
-static void
-test13 (void)
-{
-  static const Command commands[] = {
-    { INSERT_SEQ, 0, -1, "this is a test\nmore", "this is a test\nmore", SET, UNSET, UNSET },
-    { UNDO, -1, -1, NULL, "this is a test\n", SET, SET, UNSET },
-    { UNDO, -1, -1, NULL, "this is a test", SET, SET, UNSET },
-    { UNDO, -1, -1, NULL, "this is a", UNSET, SET, UNSET },
-    { UNDO, -1, -1, NULL, "this is a", UNSET, SET, UNSET },
-    { SET_MAX_UNDO, 2, -1, NULL, "this is a", UNSET, SET, UNSET },
-    { REDO, -1, -1, NULL, "this is a test", SET, SET, UNSET },
-    { REDO, -1, -1, NULL, "this is a test\n", SET, UNSET, UNSET },
-    { REDO, -1, -1, NULL, "this is a test\n", SET, UNSET, UNSET },
-  };
-
-  run_test (commands, G_N_ELEMENTS (commands), 3);
-}
-
-int
-main (int   argc,
-      char *argv[])
-{
-  g_test_init (&argc, &argv, NULL);
-  g_test_add_func ("/Gtk/TextHistory/test1", test1);
-  g_test_add_func ("/Gtk/TextHistory/test2", test2);
-  g_test_add_func ("/Gtk/TextHistory/test3", test3);
-  g_test_add_func ("/Gtk/TextHistory/test4", test4);
-  g_test_add_func ("/Gtk/TextHistory/test5", test5);
-  g_test_add_func ("/Gtk/TextHistory/test6", test6);
-  g_test_add_func ("/Gtk/TextHistory/test7", test7);
-  g_test_add_func ("/Gtk/TextHistory/test8", test8);
-  g_test_add_func ("/Gtk/TextHistory/test9", test9);
-  g_test_add_func ("/Gtk/TextHistory/test10", test10);
-  g_test_add_func ("/Gtk/TextHistory/test11", test11);
-  g_test_add_func ("/Gtk/TextHistory/test12", test12);
-  g_test_add_func ("/Gtk/TextHistory/test13", test13);
-  return g_test_run ();
-}
index a33fd29c1e3b3de5ea3fd8e7c9879b42396ce145..9fe45d339b45843089a9150ea51a7bd698343844 100644 (file)
@@ -117,6 +117,7 @@ internal_tests = [
   { 'name': 'propertylookuplistmodel' },
   { 'name': 'rbtree' },
   { 'name': 'timsort' },
+  { 'name': 'texthistory' },
 ]
 
 # Tests that are expected to fail
diff --git a/testsuite/gtk/texthistory.c b/testsuite/gtk/texthistory.c
new file mode 100644 (file)
index 0000000..f9fe45c
--- /dev/null
@@ -0,0 +1,602 @@
+#include "gtktexthistoryprivate.h"
+
+#if 0
+# define DEBUG_COMMANDS
+#endif
+
+typedef struct
+{
+  GtkTextHistory *history;
+  GString *buf;
+  struct {
+    int insert;
+    int bound;
+  } selection;
+  guint can_redo : 1;
+  guint can_undo : 1;
+  guint is_modified : 1;
+} Text;
+
+enum {
+  IGNORE = 0,
+  SET = 1,
+  UNSET = 2,
+};
+
+enum {
+  IGNORE_SELECT = 0,
+  DO_SELECT = 1,
+};
+
+enum {
+  INSERT = 1,
+  INSERT_SEQ,
+  BACKSPACE,
+  DELETE_KEY,
+  UNDO,
+  REDO,
+  BEGIN_IRREVERSIBLE,
+  END_IRREVERSIBLE,
+  BEGIN_USER,
+  END_USER,
+  MODIFIED,
+  UNMODIFIED,
+  SELECT,
+  CHECK_SELECT,
+  SET_MAX_UNDO,
+};
+
+typedef struct
+{
+  int kind;
+  int location;
+  int end_location;
+  const char *text;
+  const char *expected;
+  int can_undo;
+  int can_redo;
+  int is_modified;
+  int select;
+} Command;
+
+static void
+do_change_state (gpointer funcs_data,
+                 gboolean is_modified,
+                 gboolean can_undo,
+                 gboolean can_redo)
+{
+  Text *text = funcs_data;
+
+  text->is_modified = is_modified;
+  text->can_undo = can_undo;
+  text->can_redo = can_redo;
+}
+
+static void
+do_insert (gpointer    funcs_data,
+           guint       begin,
+           guint       end,
+           const char *text,
+           guint       len)
+{
+  Text *t = funcs_data;
+
+#ifdef DEBUG_COMMANDS
+  g_printerr ("Insert Into '%s' (begin=%u end=%u text=%s)\n",
+              t->buf->str, begin, end, text);
+#endif
+
+  g_string_insert_len (t->buf, begin, text, len);
+}
+
+static void
+do_delete (gpointer     funcs_data,
+           guint        begin,
+           guint        end,
+           const char *expected_text,
+           guint        len)
+{
+  Text *t = funcs_data;
+
+#ifdef DEBUG_COMMANDS
+  g_printerr ("Delete(begin=%u end=%u expected_text=%s)\n", begin, end, expected_text);
+#endif
+
+  if (end < begin)
+    {
+      guint tmp = end;
+      end = begin;
+      begin = tmp;
+    }
+
+  g_assert_cmpint (memcmp (t->buf->str + begin, expected_text, len), ==, 0);
+
+  if (end >= t->buf->len)
+    {
+      t->buf->len = begin;
+      t->buf->str[begin] = 0;
+      return;
+    }
+
+  memmove (t->buf->str + begin,
+           t->buf->str + end,
+           t->buf->len - end);
+  g_string_truncate (t->buf, t->buf->len - (end - begin));
+}
+
+static void
+do_select (gpointer funcs_data,
+           int      selection_insert,
+           int      selection_bound)
+{
+  Text *text = funcs_data;
+
+  text->selection.insert = selection_insert;
+  text->selection.bound = selection_bound;
+}
+
+static GtkTextHistoryFuncs funcs = {
+  do_change_state,
+  do_insert,
+  do_delete,
+  do_select,
+};
+
+static Text *
+text_new (void)
+{
+  Text *text = g_slice_new0 (Text);
+
+  text->history = gtk_text_history_new (&funcs, text);
+  text->buf = g_string_new (NULL);
+  text->selection.insert = -1;
+  text->selection.bound = -1;
+
+  return text;
+}
+
+static void
+text_free (Text *text)
+{
+  g_object_unref (text->history);
+  g_string_free (text->buf, TRUE);
+  g_slice_free (Text, text);
+}
+
+static void
+command_insert (const Command *cmd,
+                Text          *text)
+{
+  do_insert (text,
+             cmd->location,
+             cmd->location + g_utf8_strlen (cmd->text, -1),
+             cmd->text,
+             strlen (cmd->text));
+  gtk_text_history_text_inserted (text->history, cmd->location, cmd->text, -1);
+}
+
+static void
+command_delete_key (const Command *cmd,
+                    Text          *text)
+{
+  do_delete (text,
+             cmd->location,
+             cmd->end_location,
+             cmd->text,
+             strlen (cmd->text));
+  gtk_text_history_text_deleted (text->history,
+                                 cmd->location,
+                                 cmd->end_location,
+                                 cmd->text,
+                                 ABS (cmd->end_location - cmd->location));
+}
+
+static void
+command_undo (const Command *cmd,
+              Text          *text)
+{
+  gtk_text_history_undo (text->history);
+}
+
+static void
+command_redo (const Command *cmd,
+              Text          *text)
+{
+  gtk_text_history_redo (text->history);
+}
+
+static void
+set_selection (Text *text,
+               int   begin,
+               int   end)
+{
+  gtk_text_history_selection_changed (text->history, begin, end);
+}
+
+static void
+run_test (const Command *commands,
+          guint          n_commands,
+          guint          max_undo)
+{
+  Text *text = text_new ();
+
+  if (max_undo)
+    gtk_text_history_set_max_undo_levels (text->history, max_undo);
+
+  for (guint i = 0; i < n_commands; i++)
+    {
+      const Command *cmd = &commands[i];
+
+#ifdef DEBUG_COMMANDS
+      g_printerr ("%d: %d\n", i, cmd->kind);
+#endif
+
+      switch (cmd->kind)
+        {
+        case INSERT:
+          command_insert (cmd, text);
+          break;
+
+        case INSERT_SEQ:
+          for (guint j = 0; cmd->text[j]; j++)
+            {
+              const char seqtext[2] = { cmd->text[j], 0 };
+              Command seq = { INSERT, cmd->location + j, 1, seqtext, NULL };
+              command_insert (&seq, text);
+            }
+          break;
+
+        case DELETE_KEY:
+          if (cmd->select == DO_SELECT)
+            set_selection (text, cmd->location, cmd->end_location);
+          else if (strlen (cmd->text) == 1)
+            set_selection (text, cmd->location, -1);
+          else
+            set_selection (text, -1, -1);
+          command_delete_key (cmd, text);
+          break;
+
+        case BACKSPACE:
+          if (cmd->select == DO_SELECT)
+            set_selection (text, cmd->location, cmd->end_location);
+          else if (strlen (cmd->text) == 1)
+            set_selection (text, cmd->end_location, -1);
+          else
+            set_selection (text, -1, -1);
+          command_delete_key (cmd, text);
+          break;
+
+        case UNDO:
+          command_undo (cmd, text);
+          break;
+
+        case REDO:
+          command_redo (cmd, text);
+          break;
+
+        case BEGIN_USER:
+          gtk_text_history_begin_user_action (text->history);
+          break;
+
+        case END_USER:
+          gtk_text_history_end_user_action (text->history);
+          break;
+
+        case BEGIN_IRREVERSIBLE:
+          gtk_text_history_begin_irreversible_action (text->history);
+          break;
+
+        case END_IRREVERSIBLE:
+          gtk_text_history_end_irreversible_action (text->history);
+          break;
+
+        case MODIFIED:
+          gtk_text_history_modified_changed (text->history, TRUE);
+          break;
+
+        case UNMODIFIED:
+          gtk_text_history_modified_changed (text->history, FALSE);
+          break;
+
+        case SELECT:
+          gtk_text_history_selection_changed (text->history,
+                                              cmd->location,
+                                              cmd->end_location);
+          break;
+
+        case CHECK_SELECT:
+          g_assert_cmpint (text->selection.insert, ==, cmd->location);
+          g_assert_cmpint (text->selection.bound, ==, cmd->end_location);
+          break;
+
+        case SET_MAX_UNDO:
+          /* Not ideal use of location, but fine */
+          gtk_text_history_set_max_undo_levels (text->history, cmd->location);
+          break;
+
+        default:
+          break;
+        }
+
+      if (cmd->expected)
+        g_assert_cmpstr (text->buf->str, ==, cmd->expected);
+
+      if (cmd->can_redo == SET)
+        g_assert_cmpint (text->can_redo, ==, TRUE);
+      else if (cmd->can_redo == UNSET)
+        g_assert_cmpint (text->can_redo, ==, FALSE);
+
+      if (cmd->can_undo == SET)
+        g_assert_cmpint (text->can_undo, ==, TRUE);
+      else if (cmd->can_undo == UNSET)
+        g_assert_cmpint (text->can_undo, ==, FALSE);
+
+      if (cmd->is_modified == SET)
+        g_assert_cmpint (text->is_modified, ==, TRUE);
+      else if (cmd->is_modified == UNSET)
+        g_assert_cmpint (text->is_modified, ==, FALSE);
+    }
+
+  text_free (text);
+}
+
+static void
+test1 (void)
+{
+  static const Command commands[] = {
+    { INSERT, 0, -1, "test", "test", SET, UNSET },
+    { INSERT, 2, -1, "s", "tesst", SET, UNSET },
+    { INSERT, 3, -1, "ss", "tesssst", SET, UNSET },
+    { DELETE_KEY, 2, 5, "sss", "test", SET, UNSET },
+    { UNDO, -1, -1, NULL, "tesssst", SET, SET },
+    { REDO, -1, -1, NULL, "test", SET, UNSET },
+    { UNDO, -1, -1, NULL, "tesssst", SET, SET },
+    { DELETE_KEY, 0, 7, "tesssst", "", SET, UNSET },
+    { INSERT, 0, -1, "z", "z", SET, UNSET },
+    { UNDO, -1, -1, NULL, "", SET, SET },
+    { UNDO, -1, -1, NULL, "tesssst", SET, SET },
+    { UNDO, -1, -1, NULL, "test", SET, SET },
+  };
+
+  run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test2 (void)
+{
+  static const Command commands[] = {
+    { BEGIN_IRREVERSIBLE, -1, -1, NULL, "", UNSET, UNSET },
+    { INSERT, 0, -1, "this is a test", "this is a test", UNSET, UNSET },
+    { END_IRREVERSIBLE, -1, -1, NULL, "this is a test", UNSET, UNSET },
+    { UNDO, -1, -1, NULL, "this is a test", UNSET, UNSET },
+    { REDO, -1, -1, NULL, "this is a test", UNSET, UNSET },
+    { BEGIN_USER, -1, -1, NULL, NULL, UNSET, UNSET },
+    { INSERT, 0, -1, "first", "firstthis is a test", UNSET, UNSET },
+    { INSERT, 5, -1, " ", "first this is a test", UNSET, UNSET },
+    { END_USER, -1, -1, NULL, "first this is a test", SET, UNSET },
+    { UNDO, -1, -1, NULL, "this is a test", UNSET, SET },
+    { UNDO, -1, -1, NULL, "this is a test", UNSET, SET },
+    { REDO, -1, -1, NULL, "first this is a test", SET, UNSET },
+    { UNDO, -1, -1, NULL, "this is a test", UNSET, SET },
+  };
+
+  run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test3 (void)
+{
+  static const Command commands[] = {
+    { INSERT_SEQ, 0, -1, "this is a test of insertions.", "this is a test of insertions.", SET, UNSET },
+    { UNDO, -1, -1, NULL, "this is a test of", SET, SET },
+    { UNDO, -1, -1, NULL, "this is a test", SET, SET },
+    { UNDO, -1, -1, NULL, "this is a", SET, SET },
+    { UNDO, -1, -1, NULL, "this is", SET, SET },
+    { UNDO, -1, -1, NULL, "this", SET, SET },
+    { UNDO, -1, -1, NULL, "", UNSET, SET },
+    { UNDO, -1, -1, NULL, "" , UNSET, SET },
+    { REDO, -1, -1, NULL, "this", SET, SET },
+    { REDO, -1, -1, NULL, "this is", SET, SET },
+    { REDO, -1, -1, NULL, "this is a", SET, SET },
+    { REDO, -1, -1, NULL, "this is a test", SET, SET },
+    { REDO, -1, -1, NULL, "this is a test of", SET, SET },
+    { REDO, -1, -1, NULL, "this is a test of insertions.", SET, UNSET },
+  };
+
+  run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test4 (void)
+{
+  static const Command commands[] = {
+    { INSERT, 0, -1, "initial text", "initial text", SET, UNSET },
+    /* Barrier */
+    { BEGIN_IRREVERSIBLE, -1, -1, NULL, NULL, UNSET, UNSET },
+    { END_IRREVERSIBLE, -1, -1, NULL, NULL, UNSET, UNSET },
+    { INSERT, 0, -1, "more text ", "more text initial text", SET, UNSET },
+    { UNDO, -1, -1, NULL, "initial text", UNSET, SET },
+    { UNDO, -1, -1, NULL, "initial text", UNSET, SET },
+    { REDO, -1, -1, NULL, "more text initial text", SET, UNSET },
+    /* Barrier */
+    { BEGIN_IRREVERSIBLE, UNSET, UNSET },
+    { END_IRREVERSIBLE, UNSET, UNSET },
+    { UNDO, -1, -1, NULL, "more text initial text", UNSET, UNSET },
+  };
+
+  run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test5 (void)
+{
+  static const Command commands[] = {
+    { INSERT, 0, -1, "initial text", "initial text", SET, UNSET },
+    { DELETE_KEY, 0, 12, "initial text", "", SET, UNSET },
+    /* Add empty nested user action (should get ignored) */
+    { BEGIN_USER, -1, -1, NULL, NULL, UNSET, UNSET },
+      { BEGIN_USER, -1, -1, NULL, NULL, UNSET, UNSET },
+        { BEGIN_USER, -1, -1, NULL, NULL, UNSET, UNSET },
+        { END_USER, -1, -1, NULL, NULL, UNSET, UNSET },
+      { END_USER, -1, -1, NULL, NULL, UNSET, UNSET },
+    { END_USER, -1, -1, NULL, NULL, SET, UNSET },
+    { UNDO, -1, -1, NULL, "initial text" },
+  };
+
+  run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test6 (void)
+{
+  static const Command commands[] = {
+    { INSERT_SEQ, 0, -1, " \t\t    this is some text", " \t\t    this is some text", SET, UNSET },
+    { UNDO, -1, -1, NULL, " \t\t    this is some", SET, SET },
+    { UNDO, -1, -1, NULL, " \t\t    this is", SET, SET },
+    { UNDO, -1, -1, NULL, " \t\t    this", SET, SET },
+    { UNDO, -1, -1, NULL, "", UNSET, SET },
+    { UNDO, -1, -1, NULL, "", UNSET, SET },
+  };
+
+  run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test7 (void)
+{
+  static const Command commands[] = {
+    { MODIFIED, -1, -1, NULL, NULL, UNSET, UNSET, SET },
+    { UNMODIFIED, -1, -1, NULL, NULL, UNSET, UNSET, UNSET },
+    { INSERT, 0, -1, "foo bar", "foo bar", SET, UNSET, UNSET },
+    { MODIFIED, -1, -1, NULL, NULL, SET, UNSET, SET },
+    { UNDO, -1, -1, NULL, "", UNSET, SET, UNSET },
+    { REDO, -1, -1, NULL, "foo bar", SET, UNSET, SET },
+    { UNDO, -1, -1, NULL, "", UNSET, SET, UNSET },
+    { REDO, -1, -1, NULL, "foo bar", SET, UNSET, SET },
+  };
+
+  run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test8 (void)
+{
+  static const Command commands[] = {
+    { INSERT, 0, -1, "foo bar", "foo bar", SET, UNSET, UNSET },
+    { MODIFIED, -1, -1, NULL, NULL, SET, UNSET, SET },
+    { INSERT, 0, -1, "f", "ffoo bar", SET, UNSET, SET },
+    { UNMODIFIED, -1, -1, NULL, NULL, SET, UNSET, UNSET },
+    { UNDO, -1, -1, NULL, "foo bar", SET, SET, SET },
+    { UNDO, -1, -1, NULL, "", UNSET, SET, SET },
+    { REDO, -1, -1, NULL, "foo bar", SET, SET, SET },
+    { REDO, -1, -1, NULL, "ffoo bar", SET, UNSET, UNSET },
+  };
+
+  run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test9 (void)
+{
+  static const Command commands[] = {
+    { INSERT, 0, -1, "foo bar", "foo bar", SET, UNSET, UNSET },
+    { DELETE_KEY, 0, 3, "foo", " bar", SET, UNSET, UNSET, DO_SELECT },
+    { DELETE_KEY, 0, 4, " bar", "", SET, UNSET, UNSET, DO_SELECT },
+    { UNDO, -1, -1, NULL, " bar", SET, SET, UNSET },
+    { CHECK_SELECT, 0, 4, NULL, " bar", SET, SET, UNSET },
+    { UNDO, -1, -1, NULL, "foo bar", SET, SET, UNSET },
+    { CHECK_SELECT, 0, 3, NULL, "foo bar", SET, SET, UNSET },
+    { BEGIN_IRREVERSIBLE, -1, -1, NULL, "foo bar", UNSET, UNSET, UNSET },
+    { END_IRREVERSIBLE, -1, -1, NULL, "foo bar", UNSET, UNSET, UNSET },
+  };
+
+  run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test10 (void)
+{
+  static const Command commands[] = {
+    { BEGIN_USER }, { INSERT, 0, -1, "t", "t", UNSET, UNSET, UNSET }, { END_USER },
+    { BEGIN_USER }, { INSERT, 1, -1, " ", "t ", UNSET, UNSET, UNSET }, { END_USER },
+    { BEGIN_USER }, { INSERT, 2, -1, "t", "t t", UNSET, UNSET, UNSET }, { END_USER },
+    { BEGIN_USER }, { INSERT, 3, -1, "h", "t th", UNSET, UNSET, UNSET }, { END_USER },
+    { BEGIN_USER }, { INSERT, 4, -1, "i", "t thi", UNSET, UNSET, UNSET }, { END_USER },
+    { BEGIN_USER }, { INSERT, 5, -1, "s", "t this", UNSET, UNSET, UNSET }, { END_USER },
+  };
+
+  run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test11 (void)
+{
+  /* Test backspace */
+  static const Command commands[] = {
+    { INSERT_SEQ, 0, -1, "insert some text", "insert some text", SET, UNSET, UNSET },
+    { BACKSPACE, 15, 16, "t", "insert some tex", SET, UNSET, UNSET },
+    { BACKSPACE, 14, 15, "x", "insert some te", SET, UNSET, UNSET },
+    { BACKSPACE, 13, 14, "e", "insert some t", SET, UNSET, UNSET },
+    { BACKSPACE, 12, 13, "t", "insert some ", SET, UNSET, UNSET },
+    { UNDO, -1, -1, NULL, "insert some text", SET, SET, UNSET },
+  };
+
+  run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test12 (void)
+{
+  static const Command commands[] = {
+    { INSERT_SEQ, 0, -1, "this is a test\nmore", "this is a test\nmore", SET, UNSET, UNSET },
+    { UNDO, -1, -1, NULL, "this is a test\n", SET, SET, UNSET },
+    { UNDO, -1, -1, NULL, "this is a test", SET, SET, UNSET },
+    { UNDO, -1, -1, NULL, "this is a", SET, SET, UNSET },
+    { UNDO, -1, -1, NULL, "this is", SET, SET, UNSET },
+    { UNDO, -1, -1, NULL, "this", SET, SET, UNSET },
+    { UNDO, -1, -1, NULL, "", UNSET, SET, UNSET },
+  };
+
+  run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test13 (void)
+{
+  static const Command commands[] = {
+    { INSERT_SEQ, 0, -1, "this is a test\nmore", "this is a test\nmore", SET, UNSET, UNSET },
+    { UNDO, -1, -1, NULL, "this is a test\n", SET, SET, UNSET },
+    { UNDO, -1, -1, NULL, "this is a test", SET, SET, UNSET },
+    { UNDO, -1, -1, NULL, "this is a", UNSET, SET, UNSET },
+    { UNDO, -1, -1, NULL, "this is a", UNSET, SET, UNSET },
+    { SET_MAX_UNDO, 2, -1, NULL, "this is a", UNSET, SET, UNSET },
+    { REDO, -1, -1, NULL, "this is a test", SET, SET, UNSET },
+    { REDO, -1, -1, NULL, "this is a test\n", SET, UNSET, UNSET },
+    { REDO, -1, -1, NULL, "this is a test\n", SET, UNSET, UNSET },
+  };
+
+  run_test (commands, G_N_ELEMENTS (commands), 3);
+}
+
+int
+main (int   argc,
+      char *argv[])
+{
+  (g_test_init) (&argc, &argv, NULL);
+
+  g_test_add_func ("/Gtk/TextHistory/test1", test1);
+  g_test_add_func ("/Gtk/TextHistory/test2", test2);
+  g_test_add_func ("/Gtk/TextHistory/test3", test3);
+  g_test_add_func ("/Gtk/TextHistory/test4", test4);
+  g_test_add_func ("/Gtk/TextHistory/test5", test5);
+  g_test_add_func ("/Gtk/TextHistory/test6", test6);
+  g_test_add_func ("/Gtk/TextHistory/test7", test7);
+  g_test_add_func ("/Gtk/TextHistory/test8", test8);
+  g_test_add_func ("/Gtk/TextHistory/test9", test9);
+  g_test_add_func ("/Gtk/TextHistory/test10", test10);
+  g_test_add_func ("/Gtk/TextHistory/test11", test11);
+  g_test_add_func ("/Gtk/TextHistory/test12", test12);
+  g_test_add_func ("/Gtk/TextHistory/test13", test13);
+
+  return g_test_run ();
+}